/*
 * Copyright (c) 2024, salesforce.com, inc.
 * All rights reserved.
 * Licensed under the BSD 3-Clause license.
 * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
 */

import type { ConfigService } from './configService';
import type { DescribeMetadataObject } from './schemas/describeMetadataObject';
import type { SettingsService } from '../vscode/settingsService';
import type { WorkspaceService } from '../vscode/workspaceService';
import * as Effect from 'effect/Effect';
import * as S from 'effect/Schema';
import type { DescribeSObjectResult } from 'jsforce';
import { ChannelService } from '../vscode/channelService';
import { ConnectionService } from './connectionService';
import { FilePropertiesSchema, type FileProperties } from './schemas/fileProperties';

type DescribeContext = ConnectionService | ConfigService | WorkspaceService | ChannelService | SettingsService;

const NON_SUPPORTED_TYPES = new Set(['InstalledPackage', 'Profile', 'Scontrol']);

export class MetadataDescribeService extends Effect.Service<MetadataDescribeService>()('MetadataDescribeService', {
  effect: Effect.gen(function* () {
    // a task that can be cached - uses the key parameter for caching
    const cacheableDescribe = (
      _key: string = 'cached'
    ): Effect.Effect<readonly DescribeMetadataObject[], Error, DescribeContext> =>
      Effect.flatMap(ConnectionService, svc => svc.getConnection).pipe(
        Effect.flatMap(conn =>
          Effect.tryPromise({
            try: () => conn.metadata.describe(),
            catch: e => new Error(`Describe failed: ${String(e)}`)
          }).pipe(
            Effect.withSpan('describe (API call)'),
            Effect.map(result => result.metadataObjects.filter(obj => !NON_SUPPORTED_TYPES.has(obj.xmlName))),
            Effect.tap(result =>
              Effect.flatMap(ChannelService, channel =>
                channel.appendToChannel(`Metadata describe call completed. Found ${result.length} metadata types.`)
              )
            )
          )
        ),
        Effect.withSpan('cacheableDescribe')
      );

    const cachedDescribe = yield* Effect.cachedFunction(cacheableDescribe);

    const describe = (
      forceRefresh = false
    ): Effect.Effect<readonly DescribeMetadataObject[], Error, DescribeContext> =>
      forceRefresh ? cacheableDescribe(`fresh-${Date.now()}`) : cachedDescribe('cached');

    // TODO: write the result in a common place that other services can use.  Probably do the same with mdapi describe and list
    const describeCustomObject = (objectName: string): Effect.Effect<DescribeSObjectResult, Error, DescribeContext> =>
      Effect.flatMap(ConnectionService, svc => svc.getConnection).pipe(
        Effect.flatMap(conn =>
          Effect.tryPromise({
            try: () => conn.sobject(objectName).describe(),
            catch: e => new Error(`describeCustomObject failed for object ${objectName}: ${String(e)}`)
          })
        ),
        Effect.tap(result => Effect.log(result.fields.map(f => f.name).join(', '))),
        Effect.withSpan('describeCustomObject', { attributes: { objectName } })
      );

    const listMetadata = (
      type: string,
      folder?: string
    ): Effect.Effect<readonly FileProperties[], Error, DescribeContext> =>
      Effect.flatMap(ConnectionService, svc => svc.getConnection).pipe(
        Effect.flatMap(conn =>
          Effect.tryPromise({
            try: () => conn.metadata.list({ type, ...(folder ? { folder } : {}) }),
            catch: e => new Error(`listMetadata failed for type ${type}: ${String(e)}`)
          }).pipe(
            Effect.tap(result => Effect.annotateCurrentSpan({ result })),
            Effect.withSpan('listMetadata (API call)'),
            Effect.map(ensureArray),
            Effect.map(arr => arr.toSorted((a, b) => a.fullName.localeCompare(b.fullName))),
            Effect.flatMap(arr => S.decodeUnknown(S.Array(FilePropertiesSchema))(arr)),
            Effect.mapError(e => new Error(`Failed to decode FileProperties: ${String(e)}`))
          )
        ),
        Effect.withSpan('listMetadata', { attributes: { metadataType: type, folder } })
      );
    return {
      /**
       * Performs a Metadata API describe and returns the result.
       * When forceRefresh=true, bypasses the cache and makes a fresh API call.
       */
      describe,
      /**
       * Calls the Metadata API list method for a given type and optional folder.
       * Returns the list of metadata components for that type.
       */
      listMetadata,
      describeCustomObject
    } as const;
  }),
  dependencies: [ConnectionService.Default]
}) {}

const ensureArray = <T>(value: T | T[]): T[] => (Array.isArray(value) ? value : [value]);
